﻿using System;
using System.Runtime.InteropServices;
using XConductor.Domain.Seedwork.Media;
using XConductor.Domain.WP8.Media;

namespace XConductor.Domain.WP8
{
    /// <summary>
    /// An abstract base class for simplifying working with Media Foundation Transforms
    /// You need to override the method that actually creates and configures the transform
    /// </summary>
    public abstract class MediaFoundationTransform : IDisposable
    {
        protected readonly IAudioSourceProvider m_sourceProvider; 
        protected readonly IAudioFormatDescription m_outputFormatDescription;

        private readonly byte[] m_sourceBuffer;

        private byte[] m_outputBuffer;
        private int m_outputBufferOffset;
        private int m_outputBufferCount;

        private IMFTransform m_transform;
        private bool m_disposed;
        private long m_inputPosition; // in ref-time, so we can timestamp the input samples
        private long m_outputPosition; // also in ref-time
        private bool m_initializedForStreaming;

        public MediaFoundationTransform(IAudioSourceProvider sourceProvider, IAudioFormatDescription outputFormat)
        {
            this.m_outputFormatDescription = outputFormat;
            this.m_sourceProvider = sourceProvider;

            IAudioFormatDescription sourceFormatDescription = sourceProvider.AudioFormat;
            this.m_sourceBuffer = new byte[sourceFormatDescription.BytesPerFrame * sourceFormatDescription.SampleRate];
            this.m_outputBuffer = new byte[outputFormat.BytesPerFrame * outputFormat.SampleRate]; // we will grow this buffer if needed, but try to make something big enough
        }

        private void InitializeTransformForStreaming()
        {
            this.m_transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_COMMAND_FLUSH, IntPtr.Zero);
            this.m_transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, IntPtr.Zero);
            this.m_transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_NOTIFY_START_OF_STREAM, IntPtr.Zero);
            this.m_initializedForStreaming = true;
        }

        protected virtual void Dispose(bool disposing)
        {
            this.m_sourceProvider.Dispose();
            if (m_transform != null)
            {
                Marshal.ReleaseComObject(m_transform);
            }
        }

        public int Read(byte[] buffer, int offset, int count)
        {
            if (m_transform == null)
            {
                m_transform = CreateTransform();
                InitializeTransformForStreaming();
            }

            // strategy will be to always read 1 second from the source, and give it to the resampler
            int bytesWritten = 0;
            
            // read in any leftovers from last time
            if (m_outputBufferCount > 0)
            {
                bytesWritten += ReadFromOutputBuffer(buffer, offset, count - bytesWritten);
            }

            while (bytesWritten < count)
            {
                var sample = ReadFromSource();
                if (sample == null) // reached the end of our input
                {
                    // be good citizens and send some end messages:
                    EndStreamAndDrain();
                    // resampler might have given us a little bit more to return
                    bytesWritten += ReadFromOutputBuffer(buffer, offset + bytesWritten, count - bytesWritten);
                    break;
                }

                // might need to resurrect the stream if the user has read all the way to the end,
                // and then repositioned the input backwards
                if (!this.m_initializedForStreaming)
                {
                    InitializeTransformForStreaming();
                }

                // give the input to the resampler
                // can get MF_E_NOTACCEPTING if we didn't drain the buffer properly
                this.m_transform.ProcessInput(0, sample, 0);

                Marshal.ReleaseComObject(sample);

                int readFromTransform;
                // n.b. in theory we ought to loop here, although we'd need to be careful as the next time into ReadFromTransform there could
                // still be some leftover bytes in outputBuffer, which would get overwritten. Only introduce this if we find a transform that 
                // needs it. For most transforms, alternating read/write should be OK
                //do
                //{
                // keep reading from transform
                readFromTransform = ReadFromTransform();
                bytesWritten += ReadFromOutputBuffer(buffer, offset + bytesWritten, count - bytesWritten);
                //} while (readFromTransform > 0);
            }

            return bytesWritten;
        }

        private void EndStreamAndDrain()
        {
            this.m_transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_NOTIFY_END_OF_STREAM, IntPtr.Zero);
            this.m_transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_COMMAND_DRAIN, IntPtr.Zero);
            int read;
            do
            {
                read = ReadFromTransform();
            } while (read > 0);
            this.m_outputBufferCount = 0;
            this.m_outputBufferOffset = 0;
            this.m_inputPosition = 0;
            this.m_outputPosition = 0;
            this.m_transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_NOTIFY_END_STREAMING, IntPtr.Zero);
            this.m_initializedForStreaming = false;
        }

        /// <summary>
        /// Attempts to read from the transform
        /// Some useful info here:
        /// http://msdn.microsoft.com/en-gb/library/windows/desktop/aa965264%28v=vs.85%29.aspx#process_data
        /// </summary>
        /// <returns></returns>
        private int ReadFromTransform()
        {
            var outputDataBuffer = new MFT_OUTPUT_DATA_BUFFER[1];
            // we have to create our own for
            var sample = MediaFoundationApi.CreateSample();
            var pBuffer = MediaFoundationApi.CreateMemoryBuffer(this.m_outputBuffer.Length);
            sample.AddBuffer(pBuffer);
            sample.SetSampleTime(m_outputPosition); // hopefully this is not needed
            outputDataBuffer[0].pSample = sample;
            
            _MFT_PROCESS_OUTPUT_STATUS status;
            var hr = m_transform.ProcessOutput(_MFT_PROCESS_OUTPUT_FLAGS.None, 
                                             1, outputDataBuffer, out status);
            if (hr == MediaFoundationErrors.MF_E_TRANSFORM_NEED_MORE_INPUT)
            {
                Marshal.ReleaseComObject(pBuffer);
                Marshal.ReleaseComObject(sample);
                // nothing to read
                return 0;
            }
            else if (hr != 0)
            {
                Marshal.ThrowExceptionForHR(hr);
            }

            IMFMediaBuffer outputMediaBuffer;
            outputDataBuffer[0].pSample.ConvertToContiguousBuffer(out outputMediaBuffer);

            IntPtr pOutputBuffer;
            int outputBufferLength;
            int maxSize;
            outputMediaBuffer.Lock(out pOutputBuffer, out maxSize, out outputBufferLength);

            this.m_outputBuffer = new byte[outputBufferLength];
            Marshal.Copy(pOutputBuffer, this.m_outputBuffer, 0, outputBufferLength);

            this.m_outputBufferOffset = 0;
            this.m_outputBufferCount = outputBufferLength;
            outputMediaBuffer.Unlock();
            //this.m_outputPosition += BytesToNsPosition(m_outputBufferCount, WaveFormat); // hopefully not needed
            Marshal.ReleaseComObject(pBuffer);
            Marshal.ReleaseComObject(sample);
            Marshal.ReleaseComObject(outputMediaBuffer);
            return outputBufferLength;
        }

        private static long BytesToNsPosition(int bytes, IAudioFormatDescription audioFormat)
        {
            long nsPosition = (10000000L * bytes) / (audioFormat.BytesPerFrame * audioFormat.SampleRate);
            return nsPosition;
        }

        private IMFSample ReadFromSource()
        {
            // we always read a full second
            int bytesRead = this.m_sourceProvider.Read(m_sourceBuffer, 0, m_sourceBuffer.Length);
            if (bytesRead == 0) return null;

            var mediaBuffer = MediaFoundationApi.CreateMemoryBuffer(bytesRead);
            IntPtr pBuffer;
            int maxLength, currentLength;
            mediaBuffer.Lock(out pBuffer, out maxLength, out currentLength);
            Marshal.Copy(m_sourceBuffer, 0, pBuffer, bytesRead);
            mediaBuffer.Unlock();
            mediaBuffer.SetCurrentLength(bytesRead);

            var sample = MediaFoundationApi.CreateSample();
            sample.AddBuffer(mediaBuffer);
            // we'll set the time, I don't think it is needed for Resampler, but other MFTs might need it
            sample.SetSampleTime(m_inputPosition);
            long duration = BytesToNsPosition(bytesRead, this.m_sourceProvider.AudioFormat);
            sample.SetSampleDuration(duration);
            m_inputPosition += duration;
            Marshal.ReleaseComObject(mediaBuffer);
            return sample;
        }

        private int ReadFromOutputBuffer(byte[] buffer, int offset, int needed)
        {
            int bytesFromOutputBuffer = Math.Min(needed, m_outputBufferCount);
            Array.Copy(this.m_outputBuffer, m_outputBufferOffset, buffer, offset, bytesFromOutputBuffer);
            m_outputBufferOffset += bytesFromOutputBuffer;
            m_outputBufferCount -= bytesFromOutputBuffer;
            if (m_outputBufferCount == 0)
            {
                m_outputBufferOffset = 0;
            }
            return bytesFromOutputBuffer;
        }

        public void Reposition()
        {
            if (m_initializedForStreaming)
            {
                EndStreamAndDrain();
                InitializeTransformForStreaming();
            }
        }

        public void Dispose()
        {
            if (!m_disposed)
            {
                m_disposed = true;
                Dispose(true);
                GC.SuppressFinalize(this);
            }
        }

        ~MediaFoundationTransform()
        {
            Dispose(false);
        }

        protected abstract IMFTransform CreateTransform();
    }
}